// --------------------------------------------------------------------------------------------------------------------- 
// <copyright file="Forecasting.cs" company="Scrum for Team System">
//   None
// </copyright>
// <summary>
//   Defines the Forecasting type.
// </summary>
// ---------------------------------------------------------------------------------------------------------------------
namespace ScrumForTeamSystem.Metrics
{
    using System;
    using System.Runtime.InteropServices;

    /// <summary>
    /// The availabe Trend line types.
    /// </summary>
    [ComVisible(true)]
    public enum TrendType
    {
        /// <summary>
        /// No selection specifed place holder.
        /// </summary>
        None = 0,

        /// <summary>
        /// The linear regression trend.
        /// </summary>
        LinearRegression = 1,

        /// <summary>
        /// The three day moving average trend.
        /// </summary>
        ThreeDayMovingAverage = 2,

        /// <summary>
        /// The five day moving average trend.
        /// </summary>
        FiveDayMovingAverage = 3,

        /// <summary>
        /// The three day weighted average trend.
        /// </summary>
        ThreeDayWeightedMovingAverage = 4,

        /// <summary>
        /// The five day weighted average trend.
        /// </summary>
        FiveDayWeightedMovingAverage = 5,

        /// <summary>
        /// The three day exponential moving average trend.
        /// </summary>
        ThreeDayExponentialMovingAverage = 6,

        /// <summary>
        /// The five day exponential moving average trend.
        /// </summary>
        FiveDayExponentialMovingAverage = 7,

        /// <summary>
        /// The linear regression by product trend.
        /// </summary>
        ProductLinearRegression = 8
    }

    /// <summary>
    /// Forecasting class: to be used within an SSRS Report's code section. The Report must declare an instance variable of this class.
    /// </summary>
    [ComVisible(true)]
    public class Forecasting
    {
        /// <summary>
        /// The daily sprint capacity figure.
        /// </summary>
        private double? dailySprintCapacity;

        /// <summary>
        /// The last data point value.
        /// </summary>
        private double? lastCapacityValue;

        /// <summary>
        /// The modelBase object.
        /// </summary>
        private ModelBase modelBase;

        /// <summary>
        /// Gets a value indicating whether this instance has existing modelBase.
        /// </summary>
        /// <value>
        /// <c>true</c> if this instance has existing modelBase; otherwise, <c>false</c>.
        /// </value>
        private bool HasExistingModel
        {
            get
            {
                return this.modelBase != null;
            }
        }

        /// <summary>
        /// Calculates a sprintCapacity Trend point, given a Data point.
        /// </summary>
        /// <param name="workRemaining">The work remaining.</param>
        /// <param name="isIncludingWeekends">Whether weekends are to be included.</param>
        /// <param name="sprintCapacity">The original team capacity.</param>
        /// <param name="sprintStart">Sprint start date.</param>
        /// <param name="sprintEnd">Sprint end date.</param>
        /// <returns>A sprint capacity Trend point, or null.</returns>
        public double? GetNextCapacityTrendValue(
            double? workRemaining, 
            bool isIncludingWeekends, 
            double? sprintCapacity, 
            DateTime? sprintStart, 
            DateTime? sprintEnd)
        {
            // Always return null if insufficent sprint data received
            if (!HasSufficentSprintDetail(sprintCapacity, sprintStart, sprintEnd))
            {
                return null;
            }

            // If this is the first call, setup the daily sprint capacity field value
            if (!this.dailySprintCapacity.HasValue)
            {
                this.dailySprintCapacity = CalculateDailyCapacity(
                        isIncludingWeekends, sprintCapacity.Value, sprintStart.Value, sprintEnd.Value);
            }

            // If we have received a work remaining value, store it for the next call.
            if (workRemaining.HasValue)
            {
                this.lastCapacityValue = workRemaining;
            }
            else if (this.lastCapacityValue.HasValue)
            {
                // Decrese the capacity value by daily capacity
                this.lastCapacityValue -= this.dailySprintCapacity;
            }

            return this.lastCapacityValue;
        }

        /// <summary>
        /// Gets the Trend.
        /// </summary>
        /// <param name="trendType">The modelBase Trend type.</param>
        /// <param name="isAdjustedThroughLastPoint">If set to <c>true</c> if [is adjusted through last point]; otherwise <c>false</c>.</param>
        /// <param name="concatenatedBurnDown">A csv concatenated string of the burndown figures.</param>
        /// <returns>The Trend value.</returns>
        [ComVisible(true)]
        public double? GetNextTrendValue(TrendType trendType, bool isAdjustedThroughLastPoint, string concatenatedBurnDown)
        {
            if (!this.HasExistingModel)
            {
                this.modelBase = ModelFactory.CreateModel(trendType)
                        .InitialiseModel(isAdjustedThroughLastPoint, concatenatedBurnDown);
            }

            return this.modelBase.GetNextTrendValue();
        }

        /// <summary>
        /// Gets the Trend.
        /// </summary>
        /// <param name="trendTypeId">The trend Type Id.</param>
        /// <param name="isAdjustedThroughLastPoint">If set to <c>true</c> if [is adjusted through last point]; otherwise <c>false</c>.</param>
        /// <param name="concatenatedBurnDown">A csv concatenated string of the burndown figures.</param>
        /// <returns>The Trend value.</returns>
        [ComVisible(true)]
        public double? GetNextTrendValue(int trendTypeId, bool isAdjustedThroughLastPoint, string concatenatedBurnDown)
        {
            if (!Enum.IsDefined(typeof(TrendType), trendTypeId))
            {
                throw new ArgumentOutOfRangeException("trendTypeId");
            }

            return this.GetNextTrendValue((TrendType)trendTypeId, isAdjustedThroughLastPoint, concatenatedBurnDown);
        }

        /// <summary>
        /// Determines whether [the specified parameters] [has sufficent detail].
        /// </summary>
        /// <param name="capacity">The capacity.</param>
        /// <param name="sprintStart">The sprint start.</param>
        /// <param name="sprintEnd">The sprint end.</param>
        /// <returns>
        /// <c>true</c> if [the specified capacity] [has sufficent detail]; otherwise, <c>false</c>.
        /// </returns>
        private static bool HasSufficentSprintDetail(double? capacity, DateTime? sprintStart, DateTime? sprintEnd)
        {
            return capacity.HasValue && sprintStart.HasValue && sprintEnd.HasValue;
        }

        /// <summary>
        /// Calculates the team's daily capacity from the overall capacity and the number of working days in the sprint (which may or may not include weekends).
        /// </summary>
        /// <param name="includeWeekends">Whether to include weekends.</param>
        /// <param name="capacity">Original team capacity.</param>
        /// <param name="sprintStart">Sprint start date.</param>
        /// <param name="sprintEnd">Sprint end date.</param>
        /// <returns>The daily capacity.</returns>
        private static double CalculateDailyCapacity(
                bool includeWeekends, double capacity, DateTime sprintStart, DateTime sprintEnd)
        {
            var days = 0;
            while (sprintStart <= sprintEnd)
            {
                var weekday = sprintStart.DayOfWeek != DayOfWeek.Saturday && sprintStart.DayOfWeek != DayOfWeek.Sunday;
                days += weekday || includeWeekends ? 1 : 0;
                sprintStart = sprintStart.AddDays(1);
            }

            return capacity / days;
        }
    }
}